记录一次非典型迁移:从 OpenResty 到 Dokploy 的踩坑

服务器原本运行在一种相对稳态中:1Panel 管理系统进程,OpenResty 处理所有流量入口,SSL 证书通过 acme.sh 自动续期。

打破这种稳态的动因是对 Dokploy 的尝试欲。我试图在一个已经跑满业务的单机环境上,引入这套基于 Docker Swarm 的 PaaS 方案。

预想中的“平滑过渡”并未发生。随后的过程与其说是技术升级,不如说是对端口独占权、SSL 终止点以及容器网络认知的一次强制校准。


一、端口冲突与独占权的让渡

Dokploy 的安装逻辑具有很强的侵入性。脚本预设它必须拥有对流量入口的绝对控制权:

  • 80:HTTP 入口及 ACME 验证
  • 443:HTTPS 入口
  • 3000:面板访问

安装脚本的检查逻辑简单粗暴:一旦检测到端口占用,直接终止。

我的服务器现状是:OpenResty 正好占据了 80 和 443。

第一次尝试:让位

我试图将 OpenResty 迁移至 8080/8443,以此腾出主入口。这是第一次认知修正:仅仅修改主配置文件是不够的。

在修改了 /opt/1panel/.../nginx.conf 后,ss -tulnp 依然显示 80 端口被占用。排查后发现,包含在 conf.d 目录下的 00.default.conf 同样硬编码了监听端口。

这暴露了我对 1Panel 管理下的 OpenResty 配置结构不够熟悉。彻底清理所有 listen 80 配置并重启服务后,Traefik(Dokploy 的网关)才得以启动。

此时架构变成了: 公网 -> Traefik (80/443) -> ?

旧服务全部下线,等待接驳。


二、嵌套代理的失败试验

为了快速恢复旧站点,我构想了一个过渡方案:让 Traefik 将旧域名的流量转发给运行在 8080 的 OpenResty。

配置看似合理:

  • Traefik 识别 Host。
  • 转发至 http://127.0.0.1:8080

然而,浏览器返回了 NET::ERR_CERT_AUTHORITY_INVALID

原因复盘:

这里出现了双重 SSL 与 HSTS 的冲突。

  1. 旧站点通过 OpenResty 配置了 Let's Encrypt 证书,并启用了 HSTS(强制 HTTPS)。
  2. OpenResty 在 8443 上依然配置了 ssl_certificate
  3. Traefik 作为新网关,尝试建立自己的 TLS 连接(或使用自签证书)。
  4. 浏览器检测到证书链与缓存的 HSTS 策略不符,直接阻断。

结论: 在一个网络拓扑中,SSL 终止点(Termination Point)应当是唯一的。试图在 Traefik 后面再挂一个处理 SSL 的 Nginx,不仅增加了延迟,更引入了难以维护的证书链管理问题。

我放弃了“套娃”方案,决定彻底拆除 OpenResty 的入口职能,转向全面迁移。


三、构建方式的差异:以 React 项目为例

第一个迁移对象是 React 前端。我选择了 Nixpacks 作为构建工具,意图跳过编写 Dockerfile 的步骤。

问题: 部署后出现 502 Bad Gateway

排查与修正: 这是对“端口”概念的一次混淆。

  • 容器视角: Nixpacks 生成的镜像默认监听 80
  • 编排视角: 我在 Dokploy 配置中习惯性填写了 3000(受开发习惯影响)。
  • 结果: Traefik 试图将流量转发给容器的 3000 端口,但该端口并未开放。

修正配置为 80 后服务恢复。这提醒我:环境变量 PORT 是给应用看的,而 Dokploy 的端口配置是给网关路由看的。两者必须显式对齐。


四、宿主机服务的接驳:Socat 方案

对于运行在宿主机 4000 端口的后端服务(NewAPI),我不希望立即容器化。如何在 Docker 容器网络(Traefik)与宿主机网络之间建立桥梁?

直接使用 host.docker.internal 在生产环境并不总是开箱即用,且依赖 Docker 版本。

我采用了一个更原始但可控的方案:Socat 桥接容器

在 Dokploy 中创建一个服务,仅运行 alpine/socat

services:
  bridge:
    image: alpine/socat
    command: tcp-listen:80,fork,reuseaddr tcp:host.docker.internal:4000
    extra_hosts:

      - "host.docker.internal:host-gateway"

逻辑链条: 用户 -> Traefik -> Socat容器(80) -> (通过 host-gateway) -> 宿主机(4000)

在此过程中,我再次混淆了测试视角:

  • 在宿主机执行 curl http://host.docker.internal 是无效的,因为这个 DNS 仅存在于容器网络内部。
  • 必须进入容器内部测试连通性,或直接检查宿主机服务是否监听了 0.0.0.0(而非 127.0.0.1)。

五、从路径转发到子域名

最后一个迁移对象是 Todo 应用的后端,原先通过 domain.com/api/ 进行路径转发。

在 Traefik 中复刻路径剥离(StripPrefix)规则增加了配置复杂度,且容易产生路径末尾斜杠(trailing slash)的兼容性问题。

决策调整: 放弃路径反代,改用子域名 api.domain.com

虽然这需要修改前端代码中的 API Base URL 并重新构建,但从长远看,独立的子域名隔离了路由逻辑,降低了网关层的维护成本。


六、当前状态

经过数小时的折腾,服务器状态如下:

  1. 入口统一: Dokploy (Traefik) 接管了 80/443。
  2. 旧网关退场: OpenResty 仅作为遗留服务的临时容器,不再处理 SSL,随时可被替代。
  3. 服务形态混和:
  4. 纯静态前端:通过 Buildpack 托管。
  5. 宿主机后端:通过 Socat 容器桥接。
  6. 新业务:直接 Docker 化。

认知小结:

  • Ingress 必须具备排他性。 除非有非常高级的流量分发需求,否则不要尝试在同一台机器上运行两个反向代理入口。
  • 网络边界的意识。 在容器化部署中,必须时刻分清“容器内 IP”、“网桥 IP”和“宿主机 IP”。localhost 的含义随上下文变化,是导致大部分连接拒绝的根源。
  • 迁移策略。 相比于通过技术手段兼容旧架构(如双层 SSL),修改业务架构(如改用子域名)往往是更彻底的解决方案。